/*******************************************************************************
 * Copyright (c) 2007 University of Oslo.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     University of Oslo - initial API and implementation
 *******************************************************************************/

package com.cea.papyrus.diagram.sequence.sedi.figures;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.draw2d.ConnectionAnchor;
import org.eclipse.draw2d.FigureListener;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;

/**
 * Superclass for all figures that can contain anchors.
 * <p>
 * An AnchoringFigure knows how to create, contain and delete anchors on itself. It does
 * not, however, have any particular opinion on where any anchors should be. To get this
 * information, it consults AnchorContributors that are installed on the figure. Each
 * contributor have an "area of effect" and can contribute anchors in this area. Moreover,
 * contributors override eachother so that when two contributors want to contribute
 * anchors to the same area, the last one wins; any anchors contributed by the first
 * contributor are ignored. This algorithm allows i.e. ExecutionOccurences to remove
 * anchors contributed by a Lifeline and replace them with its own anchors along the edges
 * of the EO.
 * </p>
 * 
 * @author frankd@ifi.uio.no
 */
public abstract class AnchoringFigure extends SeDiSubpartFigure implements AnchorContributor {

    /** These parties contribute anchorMap to this figure. */
    private ArrayList<AnchorContributor> anchorContributors;

    /** Main registry of installed anchors. */
    private ArrayList<SeDiAnchor[]> anchorReg = new ArrayList<SeDiAnchor[]>();

    /** Supplementary map tag/ref -> anchor. */
    private Map<String, SeDiAnchor> anchorMap = new HashMap<String, SeDiAnchor>();

    /** Provides assistance for correct anchoring. */
    private MessageAccess messageAccess;

    public AnchoringFigure() {
        addAnchorContributor(this);
    }

    public void setMessageAccess(final MessageAccess access) {
        this.messageAccess = access;
    }

    public void addAnchorContributor(AnchorContributor contributor) {
        if (anchorContributors == null)
            anchorContributors = new ArrayList<AnchorContributor>();
        anchorContributors.add(contributor);
        contributor.addFigureListener(getContributorObserver());
        // updateAnchors();
        invalidate();
    }

    public void removeAnchorContributor(AnchorContributor contributor) {
        contributor.removeFigureListener(getContributorObserver());
        anchorContributors.remove(contributor);
        if (anchorContributors.isEmpty())
            anchorContributors = null;
        // updateAnchors();
        invalidate();
    }

    private FigureListener contributorObserver;

    private FigureListener getContributorObserver() {
        if (contributorObserver == null)
            contributorObserver = new FigureListener() {
                public void figureMoved(IFigure source) {
                    invalidate();
                }
            };
        return contributorObserver;
    }

    public void validate() {
        if (isValid())
            return;
        super.validate();
        updateAnchors();
    }

    private static class RemovalRecord {
        public String tag;
        public SeDiAnchor[] replacementOptions;
        public Point oldAnchorLocation;
    }

    /**
     * Updates the anchors for this figure, creating new anchors and removing anchors that
     * are no longer required, as appropriate. This method should be called when the
     * figure is resized or moved so that the anchors will stay aligned with the figure
     * appearance and with the grid.
     */
    protected void updateAnchors() {
        // First some preparations:

        // Cache the bounds for each contributor. This is the area in which
        // the contributor will contribute anchors. It is a rectangle, but
        // we actually only care about it's top and height attributes.
        final Rectangle[] cBounds = new Rectangle[anchorContributors.size()];
        for (int i = 0; i < anchorContributors.size(); ++i) {
            AnchorContributor c = anchorContributors.get(i);
            cBounds[i] = c.getAnchorContributionBounds(this);
        }

        final Rectangle figureBounds = getBounds().getCopy();
        if (getParent() != null)
            getParent().translateToAbsolute(figureBounds);

        int y = Grid.findNextGridLine(this, 0);

        // Now, the real thing. Examine all the anchors and make sure that
        // they are all valid, and that there are just enough of them.

        // Three things have to be done:
        // 1. update existing anchors, if the figure has moved
        // This amounts to moving the anchor left/right as appropriate.
        // 2. Remove existing anchors, if the figure has shrunk
        // 3. Add new anchors, if the figure has grown

        final SeDiAnchor[] NO_ANCHORS = new SeDiAnchor[0];
        List<RemovalRecord> anchorsToDelete = new ArrayList<RemovalRecord>();
        int row = 0;
        while (row < anchorReg.size() || y < getSize().height) {
            // Ask the contributors for their anchors at this row.
            // Only the last contributor has effect.
            int[] x = AnchorContributor.NO_CONTRIBUTIONS;
            AnchorContributor contributor = null;
            for (int j = 0; j < anchorContributors.size(); ++j) {
                AnchorContributor c = anchorContributors.get(j);
                if (y >= cBounds[j].y && y < cBounds[j].bottom()) {
                    x = c.contributeAnchors(this, y);
                    contributor = c;
                }
            }

            final SeDiAnchor[] rowAnchors =
                    (row < anchorReg.size()) ? anchorReg.get(row) : NO_ANCHORS;

            // Update anchor positions...
            for (int j = 0; j < Math.min(rowAnchors.length, x.length); ++j) {
                if (rowAnchors[j] != null) {
                    rowAnchors[j].setRelativeLocation(x[j], y);
                }
            }

            if (rowAnchors.length != x.length) {
                SeDiAnchor[] newAnchors = new SeDiAnchor[x.length];
                if (rowAnchors.length < newAnchors.length) {
                    // New anchors have been contributed

                    System.arraycopy(rowAnchors, 0, newAnchors, 0, rowAnchors.length);
                    for (int j = rowAnchors.length; j < newAnchors.length; ++j) {
                        String tag = Character.toString((char) ('A' + j)) + row;
                        if (anchorMap.containsKey(tag))
                            newAnchors[j] = anchorMap.get(tag);
                        else {
                            newAnchors[j] = new SeDiAnchor(this, tag);
                            anchorMap.put(tag, newAnchors[j]);
                        }
                        newAnchors[j].setRelativeLocation(x[j], y);
                        newAnchors[j].installPolicy(contributor.getAnchorPolicy());
                    }

                    for (int j = 0; j < rowAnchors.length; ++j)
                        updateMessages(rowAnchors[j].getTag(), newAnchors, rowAnchors[j]
                                .getLocation(null));
                } else {
                    // Anchors have gone out of "scope" and must be deleted

                    System.arraycopy(rowAnchors, 0, newAnchors, 0, newAnchors.length);
                    for (int j = newAnchors.length; j < rowAnchors.length; ++j) {
                        final String tag = Character.toString((char) ('A' + j)) + row;
                        RemovalRecord rec = new RemovalRecord();
                        rec.tag = tag;
                        rec.replacementOptions = newAnchors;
                        rec.oldAnchorLocation = rowAnchors[j].getLocation(null);
                        anchorsToDelete.add(rec);
                    }
                }

                if (anchorReg.size() <= row) {
                    while (anchorReg.size() < row)
                        anchorReg.add(NO_ANCHORS);
                    anchorReg.add(newAnchors);
                } else
                    anchorReg.set(row, newAnchors);
            }

            ++row;
            y += Grid.ANCHOR_DELTA;
        }

        for (RemovalRecord rec : anchorsToDelete) {
            anchorMap.remove(rec.tag);
        }
        for (RemovalRecord rec : anchorsToDelete) {
            updateMessages(rec.tag, rec.replacementOptions, rec.oldAnchorLocation);
        }

    }

    /**
     * Moves all messages that currently attach to the anchor that is represented by
     * 'tag', to one of the anchors presented in 'options'. This is normally necessary
     * when the anchor 'tag' is about to be removed from the figure. In general, the
     * anchor that minimizes the message length will be chosen. If however no anchor can
     * be found among the presented options, then the closest anchor will be chosen, no
     * matter where it is.
     * 
     * @param tag
     * @param options
     */
    private void updateMessages(final String tag, SeDiAnchor[] options, Point oldAnchorLocation) {
        MessageProxy[] msg = messageAccess.getMessages(tag);
        for (int msgnr = 0; msgnr < msg.length; ++msgnr) {
            final ConnectionAnchor targetAnchor = msg[msgnr].getOtherAnchor();

            // Choose the best anchor (which minimizes the length)
            int best = Integer.MAX_VALUE;
            int bestindex = -1;
            for (int jj = 0; jj < options.length; ++jj) {
                int dist =
                        options[jj].getLocation(null).getDistance2(targetAnchor.getLocation(null));
                if (dist <= best) {
                    best = dist;
                    bestindex = jj;
                }
            }

            if (bestindex != -1)
                msg[msgnr].updateMyAnchor(options[bestindex].getTag());
            else {
                // Pick the closest anchor in every direction
                Point targetLocation;
                SeDiAnchor currentAnchor = anchorMap.get(tag);
                if (currentAnchor != null)
                    targetLocation = currentAnchor.getLocation(null);
                else
                    targetLocation = oldAnchorLocation;

                SeDiAnchor replacementAnchor =
                        (SeDiAnchor) getAnchorAtExcept(targetLocation, currentAnchor);
                // TODO What if no replacementAnchor can be found?
                msg[msgnr].updateMyAnchor(replacementAnchor.getTag());

            }
        }
        this.getParent().getParent().getParent().invalidate();
    }

    /**
     * Finds the best target anchor for a message that is moved at the source end.
     * 
     * @param oldTargetRef
     *            Identifies the original target anchor.
     * @param distance
     *            The vertical distance that the source end was moved. We will try to find
     *            a new target anchor at the same distance from the original target
     *            anchor.
     * @param sourceX
     *            The horizontal position of the source anchor for the message. This may
     *            be used for situations when we have several anchors at a given "height",
     *            to choose the best among these.
     * @return a Tag that identifies the best new target anchor.
     */
    public String getBestTargetAnchor(String oldTargetRef, int distance, int sourceX) {
        SeDiAnchor oldAnchor = anchorMap.get(oldTargetRef);
        int i = Integer.parseInt(oldAnchor.getTag().substring(1));
        i += distance / Grid.ANCHOR_DELTA;
        if (i < 0)
            i = 0;
        else if (i >= anchorReg.size())
            i = anchorReg.size() - 1;

        SeDiAnchor[] candidates = anchorReg.get(i);
        if (distance < 0 && i < anchorReg.size())
            while (candidates.length == 0) {
                ++i;
                candidates = anchorReg.get(i);
            }
        else if (distance > 0 && i >= 0)
            while (candidates.length == 0) {
                --i;
                candidates = anchorReg.get(i);
            }

        if (i < 0 || i >= anchorReg.size() || candidates.length == 0)
            return oldTargetRef; // we couldn't find any better anchors

        int minHorizontalDistance = Integer.MAX_VALUE;
        int minIndex = 0;
        for (int k = 0; k < candidates.length; ++k) {
            int hdist = Math.abs(candidates[k].getLocation(null).x - sourceX);
            if (hdist > 0 && hdist < minHorizontalDistance) {
                minHorizontalDistance = hdist;
                minIndex = k;
            }
        }
        return candidates[minIndex].getTag();
    }

    public ConnectionAnchor getAnchorAt(final Point pt) {
        ConnectionAnchor closest = null;
        long min = Long.MAX_VALUE;
        if (anchorMap == null || anchorMap.isEmpty()) {
            updateAnchors();
        }
        for (ConnectionAnchor anchor : anchorMap.values()) {
            final Point p = anchor.getLocation(null);
            final int d = pt.getDistance2(p);
            if (d < min) {
                min = d;
                closest = anchor;
            }
        }
        return closest;
    }

    public ConnectionAnchor getAnchorAt(final Point pt, boolean right) {
        ConnectionAnchor closest = null;
        long min = Long.MAX_VALUE;
        if (anchorMap == null || anchorMap.isEmpty()) {
            updateAnchors();
        }
        for (ConnectionAnchor anchor : anchorMap.values()) {
            final Point p = anchor.getLocation(null);
            final int d = pt.getDistance2(p);
            if (d <= min || d <= min + 15) {
                if (closest == null) {
                    closest = anchor;
                    min = d;
                } else if (min > d + 15) {
                    closest = anchor;
                    min = d;
                } else if (right) {
                    if (anchor.getLocation(null).x >= closest.getLocation(null).x) {
                        closest = anchor;
                        min = d;
                    }
                } else {
                    if (anchor.getLocation(null).x < closest.getLocation(null).x) {
                        closest = anchor;
                        min = d;
                    }
                }
            }
        }
        return closest;
    }

    public ConnectionAnchor getAnchorAtExcept(final Point pt, ConnectionAnchor except) {
        ConnectionAnchor closest = null;
        long min = Long.MAX_VALUE;
        if (anchorMap == null || anchorMap.isEmpty()) {
            updateAnchors();
        }
        for (ConnectionAnchor anchor : anchorMap.values()) {
            if (anchor != except) {
                final Point p = anchor.getLocation(null);
                final int d = pt.getDistance2(p);
                if (d < min) {
                    min = d;
                    closest = anchor;
                }
            }
        }
        return closest;
    }

    public ConnectionAnchor getAnchor(String ref) {
        if (ref == null)
            throw new IllegalArgumentException("null");

        // Anchors may be requested before they have been actually created through
        // "updateAnchors", so let's create a dummy anchor to get started.
        SeDiAnchor anchor = anchorMap.get(ref);
        if (anchor == null) {
            updateAnchors();
            anchor = anchorMap.get(ref);
            if (anchor == null) {
                anchor = new SeDiAnchor(this, ref);
                anchorMap.put(ref, anchor);
            }
        }
        return anchor;
    }

    /*
     * public int getAnchorVerticalPosition( final ConnectionAnchor anchor, final List<String>
     * sendAnchors, final List<String> receiveAnchors, final boolean send) { }
     */

    /**
     * Gets the vertical position or zero-based index of the given anchor. The topmost
     * anchor is counted as nr. 0, the closest anchor below is counted as nr. 1 etc.
     * 
     * @param anchor
     *            The anchor whose vertical position is to be determined. It is assumed
     *            that the given anchor does indeed belong to the figure.
     * @param sendAnchors
     *            Identifies the anchors (using their Tag) that are the base for Send
     *            events.
     * @param receiveAnchors
     *            Identifies the anchors (using their Tag) that are the base for Receive
     *            events.
     * @param flag
     *            Specifies whether the message that want to involve the edge is a send or
     *            a receive message. In general, a receive message comes before a send
     *            messages that share the same anchor.
     * @return the closest ConnectionAnchor "in front of" the given anchor.
     */
    /*
     * public ConnectionAnchor getAnchorBefore( final ConnectionAnchor anchor, final List<String>
     * sendAnchors, final List<String> receiveAnchors, final boolean send) { final Point
     * anchorLoc = anchor.getLocation(null); ConnectionAnchor before = null; Point
     * beforeLoc = null; // A receive message comes after all anchors that are strictly
     * above // A send message comes after all anchors that are strictly above // and also
     * after any receive anchors that are at the same level for (final String tag :
     * receiveAnchors) { final ConnectionAnchor testAnchor = (ConnectionAnchor)
     * anchorMap.get(tag); final Point testLoc = testAnchor.getLocation(null); if
     * (testLoc.x == anchorLoc.x && testLoc.y <= anchorLoc.y) { if (testLoc.y <
     * anchorLoc.y && (before == null || beforeLoc.y < testLoc.y)) { before = testAnchor;
     * beforeLoc = testLoc; } else if (testLoc.y == anchorLoc.y && send) { // special
     * case: before = testAnchor; // receive and send on same line beforeLoc = testLoc; } } }
     * for (final String tag : sendAnchors) { final ConnectionAnchor testAnchor =
     * (ConnectionAnchor) anchorMap.get(tag); final Point testLoc =
     * testAnchor.getLocation(null); if (testLoc.x == anchorLoc.x && testLoc.y <
     * anchorLoc.y) { if (testLoc.y < anchorLoc.y && (before == null || beforeLoc.y <
     * testLoc.y)) { before = testAnchor; beforeLoc = testLoc; } } } return before; }
     */
}
